"""
Template System with Variable Substitution

Provides advanced template processing for configuration values, build commands,
and runtime parameters with support for environment variables, conditionals,
and complex expressions.
"""

import os
import re
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any, Union, Set, Tuple
from dataclasses import dataclass
from enum import Enum
import string

logger = logging.getLogger(__name__)


class TemplateError(Exception):
    """Exception raised during template processing."""
    pass


class VariableScope(Enum):
    """Variable scope levels for template substitution."""
    SYSTEM = "system"        # System environment variables
    BUILD = "build"          # Build configuration variables
    RUNTIME = "runtime"      # Runtime configuration variables
    EXTENSION = "extension"  # Extension-specific variables
    USER = "user"           # User-defined variables


@dataclass
class TemplateContext:
    """Context for template variable substitution."""
    variables: Dict[str, Any]
    scope: VariableScope
    parent: Optional['TemplateContext'] = None
    
    def get(self, key: str, default: Any = None) -> Any:
        """Get a variable value with scope resolution."""
        if key in self.variables:
            return self.variables[key]
        elif self.parent:
            return self.parent.get(key, default)
        else:
            return default
    
    def set(self, key: str, value: Any) -> None:
        """Set a variable in this context."""
        self.variables[key] = value
    
    def keys(self) -> Set[str]:
        """Get all available variable keys."""
        keys = set(self.variables.keys())
        if self.parent:
            keys.update(self.parent.keys())
        return keys


class TemplateProcessor:
    """
    Advanced template processor with variable substitution and conditionals.
    
    Supports:
    - ${VAR} basic substitution
    - ${VAR:-default} default values
    - ${VAR:?error} required variables
    - ${{expression}} Python expressions
    - Conditional blocks: {% if condition %}...{% endif %}
    - Loops: {% for var in list %}...{% endfor %}
    """
    
    def __init__(self):
        """Initialize the template processor."""
        self.context_stack: List[TemplateContext] = []
        self.built_in_functions = self._create_builtin_functions()
        
    def create_context(self, scope: VariableScope, variables: Optional[Dict[str, Any]] = None) -> TemplateContext:
        """Create a new template context."""
        parent = self.context_stack[-1] if self.context_stack else None
        return TemplateContext(
            variables=variables or {},
            scope=scope,
            parent=parent
        )
        
    def push_context(self, context: TemplateContext) -> None:
        """Push a context onto the stack."""
        self.context_stack.append(context)
        
    def pop_context(self) -> Optional[TemplateContext]:
        """Pop a context from the stack."""
        return self.context_stack.pop() if self.context_stack else None
        
    def get_current_context(self) -> Optional[TemplateContext]:
        """Get the current context."""
        return self.context_stack[-1] if self.context_stack else None
        
    def process_template(self, template: Union[str, Dict, List], context: Optional[TemplateContext] = None) -> Any:
        """
        Process a template with variable substitution.
        
        Args:
            template: Template string, dict, or list to process
            context: Optional context to use (uses current if not provided)
            
        Returns:
            Processed template with variables substituted
        """
        if context:
            self.push_context(context)
            
        try:
            return self._process_value(template)
        finally:
            if context:
                self.pop_context()
                
    def _process_value(self, value: Any) -> Any:
        """Process a single value (recursive for complex types)."""
        if isinstance(value, str):
            return self._process_string(value)
        elif isinstance(value, dict):
            return {k: self._process_value(v) for k, v in value.items()}
        elif isinstance(value, list):
            return [self._process_value(item) for item in value]
        else:
            return value
            
    def _process_string(self, template: str) -> str:
        """Process a string template."""
        if not template or not isinstance(template, str):
            return template
            
        # Process control structures first (if/for blocks)
        template = self._process_control_structures(template)
        
        # Process variable substitutions
        template = self._process_variable_substitutions(template)
        
        # Process expressions
        template = self._process_expressions(template)
        
        return template
        
    def _process_control_structures(self, template: str) -> str:
        """Process control structures like if/for blocks."""
        # Process if blocks
        template = self._process_if_blocks(template)
        
        # Process for blocks
        template = self._process_for_blocks(template)
        
        return template
        
    def _process_if_blocks(self, template: str) -> str:
        """Process {% if condition %}...{% endif %} blocks."""
        pattern = r'{%\s*if\s+([^%]+)\s*%}(.*?){%\s*endif\s*%}'
        
        def replace_if_block(match):
            condition = match.group(1).strip()
            content = match.group(2)
            
            try:
                if self._evaluate_condition(condition):
                    return self._process_string(content)
                else:
                    return ""
            except Exception as e:
                logger.warning(f"Error evaluating if condition '{condition}': {e}")
                return content  # Return content if condition fails
                
        return re.sub(pattern, replace_if_block, template, flags=re.DOTALL)
        
    def _process_for_blocks(self, template: str) -> str:
        """Process {% for var in iterable %}...{% endfor %} blocks."""
        pattern = r'{%\s*for\s+(\w+)\s+in\s+([^%]+)\s*%}(.*?){%\s*endfor\s*%}'
        
        def replace_for_block(match):
            var_name = match.group(1).strip()
            iterable_expr = match.group(2).strip()
            content = match.group(3)
            
            try:
                iterable = self._evaluate_expression(iterable_expr)
                if not hasattr(iterable, '__iter__') or isinstance(iterable, str):
                    logger.warning(f"For loop expression '{iterable_expr}' is not iterable")
                    return content
                    
                results = []
                context = self.get_current_context()
                
                for item in iterable:
                    # Create temporary context with loop variable
                    loop_context = TemplateContext(
                        variables={var_name: item},
                        scope=VariableScope.USER,
                        parent=context
                    )
                    self.push_context(loop_context)
                    
                    try:
                        processed_content = self._process_string(content)
                        results.append(processed_content)
                    finally:
                        self.pop_context()
                        
                return "".join(results)
                
            except Exception as e:
                logger.warning(f"Error processing for loop '{iterable_expr}': {e}")
                return content
                
        return re.sub(pattern, replace_for_block, template, flags=re.DOTALL)
        
    def _process_variable_substitutions(self, template: str) -> str:
        """Process ${VAR} style variable substitutions."""
        # Pattern for ${VAR}, ${VAR:-default}, ${VAR:?error}
        pattern = r'\$\{([^}]+)\}'
        
        def replace_variable(match):
            var_expr = match.group(1)
            return self._resolve_variable_expression(var_expr)
            
        return re.sub(pattern, replace_variable, template)
        
    def _resolve_variable_expression(self, expr: str) -> str:
        """Resolve a variable expression like VAR, VAR:-default, VAR:?error."""
        # Check for default value syntax: VAR:-default
        if ':-' in expr:
            var_name, default_value = expr.split(':-', 1)
            var_name = var_name.strip()
            default_value = default_value.strip()
            
            context = self.get_current_context()
            if context:
                value = context.get(var_name)
                if value is not None and value != "":
                    return str(value)
                else:
                    return self._process_string(default_value)  # Process default value recursively
            else:
                return default_value
                
        # Check for required variable syntax: VAR:?error
        elif ':?' in expr:
            var_name, error_msg = expr.split(':?', 1)
            var_name = var_name.strip()
            error_msg = error_msg.strip() or f"Required variable {var_name} is not set"
            
            context = self.get_current_context()
            if context:
                value = context.get(var_name)
                if value is not None and value != "":
                    return str(value)
                else:
                    raise TemplateError(error_msg)
            else:
                raise TemplateError(error_msg)
                
        # Simple variable lookup
        else:
            var_name = expr.strip()
            context = self.get_current_context()
            if context:
                value = context.get(var_name)
                if value is not None:
                    return str(value)
                    
            # Fall back to environment variables
            env_value = os.environ.get(var_name)
            if env_value is not None:
                return env_value
                
            # Variable not found
            logger.warning(f"Variable '{var_name}' not found in context")
            return f"${{{var_name}}}"  # Return original if not found
            
    def _process_expressions(self, template: str) -> str:
        """Process ${{expression}} Python expressions."""
        pattern = r'\$\{\{([^}]+)\}\}'
        
        def replace_expression(match):
            expr = match.group(1).strip()
            try:
                result = self._evaluate_expression(expr)
                return str(result) if result is not None else ""
            except Exception as e:
                logger.warning(f"Error evaluating expression '{expr}': {e}")
                return match.group(0)  # Return original if evaluation fails
                
        return re.sub(pattern, replace_expression, template)
        
    def _evaluate_expression(self, expr: str) -> Any:
        """Evaluate a Python expression safely."""
        context = self.get_current_context()
        
        # Build safe evaluation namespace
        namespace = {
            '__builtins__': {},  # Restrict built-ins for security
            # Safe built-in functions
            'len': len,
            'str': str,
            'int': int,
            'float': float,
            'bool': bool,
            'list': list,
            'dict': dict,
            'min': min,
            'max': max,
            'abs': abs,
            'round': round,
            # Custom functions
            **self.built_in_functions
        }
        
        # Add context variables
        if context:
            all_vars = {}
            # Collect variables from all contexts in the stack
            for ctx in reversed(self.context_stack):
                all_vars.update(ctx.variables)
            namespace.update(all_vars)
            
        # Add environment variables under 'env' namespace
        namespace['env'] = dict(os.environ)
        
        try:
            return eval(expr, namespace)
        except Exception as e:
            raise TemplateError(f"Error evaluating expression '{expr}': {e}")
            
    def _evaluate_condition(self, condition: str) -> bool:
        """Evaluate a condition expression."""
        result = self._evaluate_expression(condition)
        return bool(result)
        
    def _create_builtin_functions(self) -> Dict[str, callable]:
        """Create built-in functions available in templates."""
        def exists(path: str) -> bool:
            """Check if file or directory exists."""
            return Path(path).exists()
            
        def is_file(path: str) -> bool:
            """Check if path is a file."""
            return Path(path).is_file()
            
        def is_dir(path: str) -> bool:
            """Check if path is a directory."""
            return Path(path).is_dir()
            
        def basename(path: str) -> str:
            """Get basename of a path."""
            return Path(path).name
            
        def dirname(path: str) -> str:
            """Get directory name of a path."""
            return str(Path(path).parent)
            
        def join_paths(*paths: str) -> str:
            """Join multiple paths."""
            return str(Path(*paths))
            
        def upper(text: str) -> str:
            """Convert to uppercase."""
            return str(text).upper()
            
        def lower(text: str) -> str:
            """Convert to lowercase."""
            return str(text).lower()
            
        def replace(text: str, old: str, new: str) -> str:
            """Replace text."""
            return str(text).replace(old, new)
            
        def default(value: Any, default_value: Any) -> Any:
            """Return default value if value is None or empty."""
            if value is None or value == "":
                return default_value
            return value
            
        def which(executable: str) -> Optional[str]:
            """Find executable in PATH."""
            import shutil
            return shutil.which(executable)
            
        return {
            'exists': exists,
            'is_file': is_file,
            'is_dir': is_dir,
            'basename': basename,
            'dirname': dirname,
            'join_paths': join_paths,
            'upper': upper,
            'lower': lower,
            'replace': replace,
            'default': default,
            'which': which,
        }


class ExtensionTemplateManager:
    """
    Template manager specifically for extension configurations.
    
    Provides pre-configured contexts and convenience methods for
    processing extension templates.
    """
    
    def __init__(self):
        """Initialize the extension template manager."""
        self.processor = TemplateProcessor()
        self._setup_default_contexts()
        
    def _setup_default_contexts(self):
        """Set up default template contexts."""
        # System context with environment variables and system info
        import platform
        system_vars = {
            'OS': platform.system().lower(),
            'ARCH': platform.machine().lower(),
            'PYTHON_VERSION': platform.python_version(),
            'HOME': os.path.expanduser('~'),
            'USER': os.environ.get('USER', os.environ.get('USERNAME', 'unknown')),
            'PWD': os.getcwd(),
        }
        system_vars.update(os.environ)
        
        system_context = TemplateContext(
            variables=system_vars,
            scope=VariableScope.SYSTEM
        )
        self.processor.push_context(system_context)
        
    def process_build_config(self, build_config: Dict[str, Any], 
                           build_vars: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Process build configuration with build-specific variables."""
        build_context = self.processor.create_context(
            scope=VariableScope.BUILD,
            variables=build_vars or {}
        )
        
        return self.processor.process_template(build_config, build_context)
        
    def process_command_list(self, commands: List[str], 
                           context_vars: Optional[Dict[str, Any]] = None) -> List[str]:
        """Process a list of command strings."""
        if context_vars:
            context = self.processor.create_context(
                scope=VariableScope.RUNTIME,
                variables=context_vars
            )
            return self.processor.process_template(commands, context)
        else:
            return self.processor.process_template(commands)
            
    def process_extension_config(self, config: Dict[str, Any], 
                               extension_vars: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Process complete extension configuration."""
        extension_context = self.processor.create_context(
            scope=VariableScope.EXTENSION,
            variables=extension_vars or {}
        )
        
        return self.processor.process_template(config, extension_context)
        
    def validate_template(self, template: str) -> Tuple[bool, List[str]]:
        """Validate a template without processing it."""
        errors = []
        
        try:
            # Check for balanced braces
            brace_count = template.count('${') - template.count('}')
            if brace_count != 0:
                errors.append("Unbalanced braces in template")
                
            # Check for balanced control structures
            if_count = len(re.findall(r'{%\s*if\s+', template))
            endif_count = len(re.findall(r'{%\s*endif\s*%}', template))
            if if_count != endif_count:
                errors.append("Unbalanced if/endif blocks")
                
            for_count = len(re.findall(r'{%\s*for\s+', template))
            endfor_count = len(re.findall(r'{%\s*endfor\s*%}', template))
            if for_count != endfor_count:
                errors.append("Unbalanced for/endfor blocks")
                
            # Try to find undefined variables
            var_pattern = r'\$\{([^}:]+)(?::[^}]*)?\}'
            variables = re.findall(var_pattern, template)
            
            context = self.processor.get_current_context()
            if context:
                available_vars = context.keys()
                for var in variables:
                    if var not in available_vars and var not in os.environ:
                        errors.append(f"Undefined variable: {var}")
                        
        except Exception as e:
            errors.append(f"Template validation error: {e}")
            
        return len(errors) == 0, errors
        
    def get_available_variables(self) -> Dict[VariableScope, List[str]]:
        """Get all available variables by scope."""
        result = {}
        
        for context in self.processor.context_stack:
            scope_vars = list(context.variables.keys())
            if context.scope in result:
                result[context.scope].extend(scope_vars)
            else:
                result[context.scope] = scope_vars
                
        return result
        
    def create_template_documentation(self) -> str:
        """Generate documentation for available template features."""
        doc_lines = [
            "# GSM Template System Documentation",
            "",
            "## Variable Substitution",
            "- `${VAR}` - Simple variable substitution",
            "- `${VAR:-default}` - Use default value if VAR is empty/undefined",
            "- `${VAR:?error}` - Raise error if VAR is empty/undefined",
            "",
            "## Expressions",
            "- `${{expression}}` - Python expression evaluation",
            "- Example: `${{len(some_list)}}`, `${{env.HOME + '/bin'}}`",
            "",
            "## Control Structures",
            "- `{% if condition %}...{% endif %}` - Conditional blocks",
            "- `{% for var in iterable %}...{% endfor %}` - Loop blocks",
            "",
            "## Built-in Functions",
            "- `exists(path)` - Check if file/directory exists",
            "- `is_file(path)`, `is_dir(path)` - Path type checks",
            "- `basename(path)`, `dirname(path)` - Path manipulation",
            "- `join_paths(...)` - Join multiple paths",
            "- `upper(text)`, `lower(text)` - String case conversion",
            "- `replace(text, old, new)` - String replacement",
            "- `default(value, default)` - Default value helper",
            "- `which(executable)` - Find executable in PATH",
            "",
            "## Available Variables by Scope",
        ]
        
        for scope, variables in self.get_available_variables().items():
            doc_lines.extend([
                f"### {scope.value.title()} Variables",
                ""
            ])
            for var in sorted(variables):
                doc_lines.append(f"- `{var}`")
            doc_lines.append("")
            
        return "\n".join(doc_lines)